import Konva from "konva";
import { Draw } from "./Draw";
import playSVG from "../Assets/play.svg";
import pauseSVG from "../Assets/pause.svg";
import { IKeyMap } from "../Interface/Pptx";
import { IKonva } from "../Interface/IKonva";
import { getImageSource } from "../Utils/konva";
import { getColorMatch, getUniqueId } from "../Utils";
import { lineClick } from "../Event/handle/konva/line-click";
import { KONVA_GRAPH_NAME, KONVA_LINE_NAME } from "../Config";
import { groupClick } from "../Event/handle/konva/group-click";
import { lineDragEnd } from "../Event/handle/konva/line-dragend";
import { groupDbclick } from "../Event/handle/konva/group-dbclick";
import { groupDragend } from "../Event/handle/konva/group-dragend";
import { setStageCursor } from "../Event/handle/konva/stage-cursor";
import { lineDragStart } from "../Event/handle/konva/line-dragstart";
import { groupDragMove } from "../Event/handle/konva/group-dragmove";
import { groupDragStart } from "../Event/handle/konva/group-dragstart";
import { lineMouseEnter } from "../Event/handle/konva/line-mouseenter";
import { lineMouseLeave } from "../Event/handle/konva/line-mouseleave";
import { groupMouseEnter } from "../Event/handle/konva/group-mouseenter";
import { groupMouseLeave } from "../Event/handle/konva/group-mouseleave";
import { groupTableDbclick } from "../Event/handle/konva/group-table-dbclick";
import { groupEchartDbclick } from "../Event/handle/konva/group-echart-dbclick";
import { animationLabelClick } from "../Event/handle/konva/animation-label-click";
import { groupRichTextDbClick } from "../Event/handle/konva/group-rich-text-dbclick";

/**
 * 重写 konva 图形基类，需要添加自定义事件，以实现更加丰富的功能
 *  1. 单击添加缩放、平移功能
 *  2. 双击可编辑文本
 *  3. 缩放后的尺寸问题纠正、位置纠正
 *  4. 提供 addShape 方法，将创建的图形添加到 layer 上
 *  5. 为了识别方便，需要在每一个  group 上，重新标记当前图形类型 ，定义为 shapeType
 *      1. Logo Logo
 *      2. Ring 圆环
 *      3. Star 五角星
 *      4. Rect 矩形
 *      5. Circle 圆形
 *      6. Ellipse 椭圆
 *      7. Triangle 三角形
 *      8. Image 图片
 *      9. Video 视频
 *      10. Text 文本框
 *      11. Table - 表格
 *      12. Echart - 统计图
 *      13. Arrow 箭头
 *      14. RichText 富文本
 *
 */

export class KonvaGraph {
  private draw: Draw;
  private nodeTextColor: string;
  private nodeFillColor: string;
  private nodeStrokeColor: string;

  constructor(draw: Draw) {
    this.draw = draw;

    // 初始化基础颜色
    this.nodeFillColor = <string>getColorMatch(this.draw, "nodeFillColor");
    this.nodeStrokeColor = <string>getColorMatch(this.draw, "nodeStrokeColor");
    this.nodeTextColor = <string>getColorMatch(this.draw, "nodeTextColor");
  }

  // Logo
  public Logo(payload: Konva.ShapeConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Logo" });
    const logo = new Konva.Shape({
      ...payload,
      x: 0,
      y: 0,
      fill: undefined,
      /** 自定义LOGO实现方法 */
      sceneFunc: this.getLogoOptions.bind(this),
      // 绘制事件响应区域
      hitFunc: this.hitFunc,
    });

    this.overwriteGraph(group.add(logo));
    return group;
  }

  // Ring 圆环
  public Ring(payload: Konva.RingConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Ring" });
    const ring = new Konva.Ring({
      fill: this.nodeFillColor,
      stroke: this.nodeStrokeColor,
      ...payload,
      x: 0,
      y: 0,
    });
    const text = this.getGroupText(payload);
    this.overwriteGraph(group.add(ring, text));
    return group;
  }

  // Star 五角星
  public Star(payload: Konva.StarConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Star" });
    const star = new Konva.Star({
      fill: this.nodeFillColor,
      stroke: this.nodeStrokeColor,
      ...payload,
      x: 0,
      y: 0,
    });
    const text = this.getGroupText(payload);
    this.overwriteGraph(group.add(star, text));
    return group;
  }

  // Rect 矩形
  public Rect(payload: Konva.RectConfig) {
    // 创建分组(分组的坐标会使得原件的坐标发生变化，使用时，特别注意，将组内的元素位置重置 x = 0 y = 0)
    const group = this.getGroup({ ...payload, shapeType: "Rect" });
    // 创建konva原型 【修正坐标】
    const fill = this.nodeFillColor;
    const stroke = this.nodeStrokeColor;
    const rect = new Konva.Rect({ fill, stroke, ...payload, x: 0, y: 0 });
    // 创建默认文字
    const text = this.getGroupText(payload);
    // 添加注意顺序，后添加的 ZIndex 大
    this.overwriteGraph(group.add(rect, text));
    return group;
  }

  // Circle 圆形
  public Circle(payload: Konva.CircleConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Circle" });
    const circle = new Konva.Circle({
      fill: this.nodeFillColor,
      stroke: this.nodeStrokeColor,
      ...payload,
      // 对于圆来说 偏移不再是 0 ，而是偏移圆心
      x: payload.radius,
      y: payload.radius,
    });
    const text = this.getGroupText(payload);
    this.overwriteGraph(group.add(circle, text));
    return group;
  }

  // Ellipse 椭圆
  public Ellipse(payload: Konva.EllipseConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Ellipse" });
    const ellipse = new Konva.Ellipse({
      fill: this.nodeFillColor,
      stroke: this.nodeStrokeColor,
      ...payload,
      x: payload.radiusX,
      y: payload.radiusY,
    });
    const text = this.getGroupText(payload);
    this.overwriteGraph(group.add(ellipse, text));
    return group;
  }

  // Triangle 三角形
  public Triangle(payload: Konva.ShapeConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Triangle" });
    const logo = new Konva.Shape({
      ...payload,
      x: 0,
      y: 0,
      fill: undefined,
      /** 定义统计图样式 */
      sceneFunc: this.getTriangleOption.bind(this),
      // 绘制事件响应区域
      hitFunc: this.hitFunc,
    });

    this.overwriteGraph(group.add(logo));
    return group;
  }

  // Image 图片
  public async Image(payload: IKonva.ImageConfig) {
    // 解析图片资源 File、Blob 均创建 FileReader 读取，string 则默认url
    const source = await getImageSource(payload.source);

    return new Promise<Konva.Group>((resolve) => {
      const image = new Image();
      image.src = source;
      // 图片的处理需要基于 image.onload 事件回调
      image.onload = () => {
        // 转换 image 宽高，避免超出编辑区 - 等比缩放
        const width = 200;
        const height = width / (image.width / image.height);

        /**
         * 解析 payload 中的参数对象，
         * 判断 x,y,width,height,
         * 后面的参数会直接覆盖前面，不需要 || 判断
         * 注意参数的顺序！后面的覆盖前面的，因此，Image x,y 都应该是0
         * */
        const groupOption = { x: 0, y: 0, width, height };

        const group = this.getGroup({
          shapeType: "Image",
          ...groupOption,
          ...payload,
          source,
        });
        const imageOption = { ...payload, image, x: 0, y: 0 };
        const konvaImage = new Konva.Image({ width, height, ...imageOption });
        this.overwriteGraph(group.add(konvaImage));
        resolve(group);
      };
    });
  }

  // Video 视频
  public async Video(payload: IKonva.ImageConfig) {
    // 解析图片资源 File、Blob 均创建 FileReader 读取，string 则默认url
    const source = await getImageSource(payload.source);

    const video = document.createElement("video");
    video.src = source;

    return new Promise<Konva.Group>((resolve) => {
      // 监听 loadedmetadata 事件，返回音视频对象
      video.addEventListener("loadedmetadata", () => {
        // 解析视频宽高
        const width = video.videoWidth;
        const height = video.videoHeight;

        const groupOption = { x: 0, y: 0, width, height }; // 默认配置，用户可覆盖
        const group = this.getGroup({
          shapeType: "Video",
          ...groupOption,
          ...payload,
          source,
        });

        // 创建 video 资源
        const vo = { ...payload, image: video, x: 0, y: 0, fill: "#000" };
        const wh = { width, height };
        const konvaVideo = new Konva.Image({ ...wh, ...vo });

        //  需要动画来渲染layer画布，以实现视频流畅播放 - 这里是通过 request Animation 实现的帧率渲染
        const layer = this.draw.getLayer()!;
        const animation = new Konva.Animation(() => void 0, layer);

        // 播放视频 /  暂停视频
        const play = () => (video.play(), animation.start());
        const pause = () => (video.pause(), animation.stop());

        // 添加播放/暂停按钮 - 添加前，先删除其他按钮
        function addButtons() {
          removeButtons();
          const playState = video.paused;
          Konva.Image.fromURL(playState ? playSVG : pauseSVG, insertImage);
        }

        // 鼠标移出事件 - 移除播放/暂停按钮
        function removeButtons() {
          group.findOne(".play")?.destroy();
          group.findOne(".pause")?.destroy();
        }

        // 插入图片回调
        const insertImage = (image: Konva.Image) => {
          const playState = video.paused;
          const x = 0;
          const y = height - 40;
          const imageBase = { x, y, width: 40, height: 40, opacity: 0.8 };
          image.setAttrs({ name: playState ? "play" : "pause", ...imageBase });
          image.on("mouseenter", () => setStageCursor(this.draw, "pointer"));
          image.on("mouseleave", () => setStageCursor(this.draw, "move"));
          const clickHandle = playState ? play : pause;
          image.on("click", () => (removeButtons(), clickHandle()));
          group.add(image);
        };

        group.on("mouseenter", addButtons);
        group.on("mouseleave", removeButtons);

        this.overwriteGraph(group.add(konvaVideo));
        resolve(group);
      });
    });
  }

  // Text 文本框 （因此该图形不需要进行配色）
  public Text(payload: IKonva.TextConfig) {
    // 创建文本，应该是动态宽度的，因此需要计算宽度 - 暂不考虑
    const group = this.getGroup({ ...payload, shapeType: "Text" });
    const {
      borderColor = "transparent",
      borderWidth = 1,
      borderRadius = 0,
      backgroundColor = "transparent",
    } = payload;
    // 多加一个矩形是为了实现边框背景颜色等效果，并无实际含义
    const rect = new Konva.Rect({
      ...payload,
      x: 0,
      y: 0,
      fill: backgroundColor,
      stroke: borderColor,
      strokeWidth: borderWidth,
      cornerRadius: borderRadius,
    });
    const text = new Konva.Text({
      lineHeight: 1.3, // 行高
      fontSize: 16,
      ...payload,
      ellipsis: true,
      x: 0,
      y: 0,
      onlyText: true,
    });
    this.overwriteGraph(group.add(rect, text));
    return group;
  }

  // Table - 表格
  public Table(payload: IKonva.TableConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Table" });
    const table = new Konva.Shape({
      ...payload,
      x: 0,
      y: 0,
      table_border: true, // 1. 开启斑马纹
      table_stripe: true, // 2. 开启边框
      sceneFunc: this.getTableOption.bind(this),
      hitFunc: this.hitFunc,
    });

    // 处理事件 - 双击进行数据编辑
    group.on("dblclick", (e) => groupTableDbclick(e, this.draw));
    this.overwriteGraph(group.add(table));
    return group;
  }

  // Echart - 统计图 - 通过 type 识别不同的统计图类型
  public Echart(payload: IKonva.EchartConfig) {
    // 初始化宽高，不然选择不到元素
    const group = this.getGroup({
      ...payload,
      width: 420,
      height: 280,
      shapeType: "Echart",
    });
    const echart = new Konva.Shape({
      width: 420,
      height: 280,
      ...payload,
      x: 0,
      y: 0,
      sceneFunc: this.getEchartOption.bind(this),
      hitFunc: this.hitFunc,
    });
    // 处理事件 - 双击进行数据编辑
    group.on("dblclick", (e) => groupEchartDbclick(e, this.draw));
    this.overwriteGraph(group.add(echart));
    return group;
  }

  // 参考 http://konvajs-doc.bluehymn.com/docs/sandbox/Modify_Curves_with_Anchor_Points.html 实现
  /**
   * Line - 简单线条
   *  points: [x1, y1, x2, y2, ...] 是根据当前 x y 的坐标做的偏移量！！！
   * @param payload
   * @returns
   */
  public Line(payload: IKonva.LineConfig) {
    // 解析颜色： 统一使用一种颜色即可
    const color = payload.stroke || payload.fill || this.nodeFillColor;
    // 创建线条
    const line = new Konva.Arrow({
      pointerLength: 0, // 控制是否显示箭头
      pointerWidth: 0,
      tension: 0, // 值越高，线条越弯曲
      strokeWidth: 3,
      stroke: color,
      fill: color,
      id: getUniqueId(),
      ...payload,
      points: payload.points,
      name: KONVA_LINE_NAME,
      shapeType: "Line",
      draggable: true,
    });

    this.overwriteLine(line);

    return line;
  }

  // Arrow 箭头
  public Arrow(payload: Konva.NodeConfig) {
    const group = this.getGroup({ ...payload, shapeType: "Arrow" });
    const text = this.getGroupText(payload);
    const arrow = new Konva.Shape({
      ...payload,
      fill: undefined,
      x: 0,
      y: 0,
      sceneFunc: this.getArrowOption.bind(this),
      hitFunc: this.hitFunc,
    });

    this.overwriteGraph(group.add(arrow, text));
    return group;
  }

  // RichText 富文本
  public async RichText(payload: IKonva.RichTextConfig) {
    // 解析图片资源 File、Blob 均创建 FileReader 读取，string 则默认url
    const source = await getImageSource(payload.source);
    return new Promise<Konva.Group>((resolve) => {
      const image = new Image();
      image.src = source;
      // 图片的处理需要基于 image.onload 事件回调
      image.onload = () => {
        const { width, height } = image;
        const groupOption = { x: 0, y: 0, width, height };
        const group = this.getGroup({
          ...groupOption,
          ...payload,
          source,
          shapeType: "RichText",
        });
        const imageOption = { ...payload, image, x: 0, y: 0 };
        const konvaImage = new Konva.Image({
          width,
          height,
          ...imageOption,
        });
        // 处理事件 - 双击进行数据编辑
        group.on("dblclick", (e) => groupRichTextDbClick(e, this.draw));
        this.overwriteGraph(group.add(konvaImage));
        resolve(group);
      };
    });
  }

  // 工具函数 - 给动画节点添加动画标签
  public addAnimationTag(group: Konva.Group, order: number) {
    const labelName = "konva-animation-label";
    // 实现自增文本
    const num = group.children.filter((n) => n.name() === labelName) || [];

    // simple label
    const Label = new Konva.Label({ name: labelName, x: 0, y: 0 });

    Label.add(
      new Konva.Tag({ fill: "yellow" }),
      new Konva.Text({ text: order.toString(), padding: 2, fill: "black" })
    );

    Label.offsetX(Label.width());
    Label.offsetY(Label.height() - num.length * 20);

    Label.on("click", (e) => animationLabelClick(e, this.draw));

    group.add(Label);
  }

  // 工具函数 - 删除动画节点动画标签
  public removeAnimationTag(group: Konva.Group) {
    const labelName = "konva-animation-label";
    group.find(`.${labelName}`).forEach((n) => n.destroy());
  }

  // 工具函数 - 获取所有被选中的元素
  public getSelected(): Konva.Group[] {
    const layer = this.draw.getLayer();
    if (!layer) return [];
    return <Konva.Group[]>(
      layer.find(`.${KONVA_GRAPH_NAME}`).filter((n) => n.getAttr("selected"))
    );
  }

  // 工具函数 - 获取选中的 Line
  public getLineSelected(): Konva.Line[] {
    const layer = this.draw.getLayer();
    if (!layer) return [];
    return <Konva.Line[]>(
      layer.find(`.${KONVA_LINE_NAME}`).filter((n) => n.getAttr("selected"))
    );
  }

  // 工具函数 - 获取所有 基础图形（konva-base-group）
  public getBaseGraph() {
    const layer = this.draw.getLayer();
    if (!layer) return [];
    return <Konva.Group[]>layer.find(`.${KONVA_GRAPH_NAME}`);
  }

  // 工具函数 - 获取所有基础直线 （）
  public getBaseLine() {
    const layer = this.draw.getLayer();
    if (!layer) return [];
    return <Konva.Line[]>layer.find(`.${KONVA_LINE_NAME}`);
  }

  // 工具函数 - 将本类创建的对象添加到 layer 上
  public addShape(payload: {
    shape: Konva.Group | Konva.Circle | Konva.Arrow;
  }) {
    const { shape } = payload;

    const layer = this.draw.getLayer();
    if (!layer) return;

    layer.add(shape);

    this.draw.render();
  }

  /** 辅助函数 - 获取表格的canvas */
  private getTableOption(ctx: Konva.Context, shape: Konva.Shape) {
    // 从图形属性获取表格数据 - 这个属性是通过 ...payload 添加进去的
    const { data } = shape.attrs;

    let width = 0; // 表格宽度
    let height = 0; // 表格高度
    const OFFSET = 30; // 为了保持一定的间隔
    const columnWidths: number[] = []; // 列宽
    const rowHeight = 30; // 行高

    /**
     * 计算的核心逻辑：根据 ctx.measureText("foo").width 来获取文本宽度
     *  通过每一列的最大宽度决定当前列的宽度
     */
    for (let i = 0; i < data[0].length; i++) {
      for (let j = 0; j < data.length; j++) {
        const currentText = data[j][i];
        const textWidth = ctx.measureText(currentText).width + OFFSET;
        columnWidths[i] = Math.max(textWidth * 1.3, columnWidths[i] || 0);
      }
    }

    columnWidths.forEach((i) => (width += i)); // 宽度 = 所有列宽之和

    height = data.length * rowHeight; // 高度 = 行高 * 行数

    // 设置图形大小
    shape.size({ width, height });
    shape.parent!.size({ width, height });

    /**
     * 开始绘制表格
     *  定义表格样式 - 这个是支持后期更改的，所以现在给定值即可
     *  1. 开启斑马纹
     *  2. 开启边框
     */

    for (let i = 0; i < data.length; i++) {
      for (let j = 0; j < data[i].length; j++) {
        const cellX =
          j === 0
            ? 0
            : columnWidths.reduce((prev, cur, idx) => {
                if (idx < j) return prev + cur;
                else return prev;
              }, 0);

        const cellY = i * rowHeight;
        const cellWidth = columnWidths[j];
        const cellHeight = rowHeight;

        if (shape.attrs.table_stripe) {
          // 绘制单元格边框 - 奇偶行实现斑马纹
          const table_stripeColor = <string[]>(
            getColorMatch(this.draw, "tableStripeColor")
          );
          ctx.fillStyle = i % 2 ? table_stripeColor[0] : table_stripeColor[1];
        }
        // 取表头颜色
        const table_headerColor = <string>(
          getColorMatch(this.draw, "tableHeaderFillColor")
        );
        if (i === 0) ctx.fillStyle = table_headerColor;

        ctx.fillRect(cellX, cellY, cellWidth, cellHeight);

        // 是否启用边框 - 边框颜色
        if (shape.attrs.table_border) {
          ctx.beginPath();
          ctx.strokeStyle = <string>(
            getColorMatch(this.draw, "tableBorderColor")
          );
          ctx.strokeRect(cellX, cellY, cellWidth, cellHeight);
          ctx.closePath();
        }

        // 绘制单元格内容;
        ctx.fillStyle = <string>getColorMatch(this.draw, "tableTextColor"); // 设置文本颜色为黑色
        ctx.font = "14px Arial"; // 设置字体样式
        ctx.fillText(data[i][j].toString(), cellX + 5, cellY + 20); // 绘制文本内容
      }
    }

    ctx.fillStrokeShape(shape);
  }

  /** 辅助函数 - 获取统计图配置对象 */
  private getEchartOption(ctx: Konva.Context, shape: Konva.Shape) {
    const { echartType } = shape.attrs;
    if (echartType === "line") return this.getEchartLine(ctx, shape);
    if (echartType === "bar") return this.getEchartBar(ctx, shape);
    if (echartType === "pie") return this.getEchartPie(ctx, shape);
    if (echartType === "radar") return this.getEchartRadar(ctx, shape);
  }

  /** 辅助函数 - 获取 Echart Line 配置对象 */
  private getEchartLine(ctx: Konva.Context, shape: Konva.Shape) {
    const { width, height, xAxis } = shape.attrs;
    const { series, echartTitle = "", titleAlign = "center" } = shape.attrs;

    // 控制背景颜色
    ctx.beginPath();
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, width, height);
    ctx.fill();
    ctx.strokeStyle = "transparent";
    ctx.strokeRect(0, 0, width, height);
    ctx.stroke();
    ctx.closePath();

    /**
     * 定义标题居中方式 titleAlign = "left";
     * 是否平滑 smooth = false;
     * 是否显示值 label = true;
     * 是否显示图例 legend = true;
     */

    const seriesColor = <string[]>getColorMatch(this.draw, "seriesColor")!;

    // 取整个维度的最大值
    const smax = series.map((i: IKonva.EchartSeries) => Math.max(...i.data));
    // 约定 Y轴间隔线数量
    const gridLineNumber = 5;
    // 取整，避免出现小数
    let maxValue = Math.max(...smax);
    maxValue = Math.ceil(maxValue / gridLineNumber) * gridLineNumber; // 所以 整个轴线的高度 代表的值是 ===> maxValue

    // 动态决定四周的间距
    const paddingLeft = ctx.measureText(maxValue.toString()).width + 10; // 左侧的偏移量跟Y轴文本宽度有关
    const paddingRight = 10;
    const paddingBottom = 20;
    const paddingTop = echartTitle ? 58 : 40; // 顶部的偏移量与标题高度有关

    // 绘制标题
    if (echartTitle) {
      ctx.beginPath();
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesTitleColor");
      ctx.font = "18px Arial"; // 设置字体样式
      // 计算标题宽度
      const titleWidth = ctx.measureText(echartTitle).width;
      const left = 0;
      const center = width / 2 - titleWidth / 2;
      const right = width - titleWidth - paddingRight;
      const x =
        titleAlign === "left" ? left : titleAlign === "center" ? center : right; // 标题的位置 left center right
      ctx.fillText(echartTitle, x, 18);
      ctx.fill();
      ctx.closePath();
    }

    // 整个高度是 height-paddingTop-paddingBottom ==> maxValue  每一个间隔是 value/number  ygap 表示每一个小间隔的高度
    const ygap = (height - paddingTop - paddingBottom) / gridLineNumber;
    // 绘制 Y 轴间隔线 标线值
    for (let i = 0; i < gridLineNumber; i++) {
      // 绘制间隔线
      const y = paddingTop + i * ygap;
      ctx.beginPath();
      ctx.moveTo(paddingLeft, y);
      ctx.lineTo(width - paddingRight, y); // 右侧不受影响
      ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesGridColor");
      ctx.stroke();
      ctx.closePath();

      // 绘制文本
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
      ctx.font = "14px Arial";
      const text = (maxValue / gridLineNumber) * (gridLineNumber - i);
      ctx.fillText(text.toString(), 0, y + 5);
    }

    // 绘制 X 轴、X 轴间隔线、根据 xAxis.length 绘制类别名称
    const xgap = (width - paddingLeft - paddingRight) / xAxis.length;
    for (let i = 0; i < xAxis.length; i++) {
      const x = i * xgap + paddingLeft;
      const y = paddingBottom;

      // 绘制间隔
      ctx.beginPath();
      ctx.moveTo(x, height - y);
      ctx.lineTo(x, height - y * 0.4);
      ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
      ctx.stroke();
      ctx.closePath();

      // 放里面好折叠 无其他含义，下列代码与循环无关，确保执行一次即可
      if (i === 0) {
        // 如果是最后一个，多画一条末尾的间隔线
        ctx.beginPath();
        ctx.moveTo(width - paddingRight, height - y); // 最后一条线不受影响
        ctx.lineTo(width - paddingRight, height - y * 0.4); // 最后一条线不受影响
        ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
        ctx.stroke();
        ctx.closePath();
        // 绘制轴线 X 轴 -
        ctx.beginPath();
        ctx.moveTo(paddingLeft, height - paddingBottom);
        ctx.lineTo(width - paddingRight, height - paddingBottom); // 右侧不受影响
        ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
        ctx.stroke();
      }
      // 绘制文本
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
      ctx.font = "14px Arial"; // 设置字体样式
      // 计算文本宽度
      const textWidth = ctx.measureText(xAxis[i]).width;
      const textX = paddingLeft + i * xgap + xgap / 2 - textWidth / 2;
      const textY = height - y * 0.2;
      ctx.fillText(xAxis[i], textX, textY);
    }

    // 绘制 图例 - 根据 series 的 name 属性生成
    const allTextWidth = series.reduce(
      (pre: number, cur: IKonva.EchartSeries) => {
        const textWidth = ctx.measureText(cur.name).width + 40;
        return pre + textWidth;
      },
      0
    );

    series.forEach((item: IKonva.EchartSeries, index: number) => {
      const name = item.name;
      const y = echartTitle ? 36 : 18;
      // 获取当前文本宽度
      const getTextWidth = (ci: number) => {
        let allX = 0;
        for (let i = 0; i < ci; i++) {
          allX += ctx.measureText(series[i].name).width + 40;
        }
        return allX;
      };
      // x 坐标动态生成- 前面所有宽度之和
      const x = width / 2 - allTextWidth / 2 + getTextWidth(index);

      // 1. 绘制圆圈
      ctx.beginPath();
      ctx.strokeStyle = seriesColor[index];
      ctx.arc(x + 10, y, 5, 0, 2 * Math.PI);
      ctx.stroke();
      ctx.closePath();
      // 绘制直线
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x + 20, y);
      ctx.stroke();
      ctx.closePath();
      // 绘制文本
      ctx.fillStyle = seriesColor[index];
      ctx.fillText(name, x + 30, y + 5);
    });

    // 绘制折线
    const radius = 4;
    series.forEach((item: IKonva.EchartSeries, index: number) => {
      const color = seriesColor[index];
      const points = <{ x: number; y: number }[]>[];
      item.data.forEach((value, i) => {
        ctx.beginPath();
        const x = paddingLeft + xgap * i + xgap / 2 - radius / 2;
        /**
         * 跟 value 有关-并且是反向的哈
         * paddingBottom - paddingTop ==> maxValue
         * [height-paddingBottom,paddingTop] ==> [0,maxValue]
         */
        const h = ((height - paddingBottom - paddingTop) / maxValue) * value;
        const y = height - paddingBottom - h;
        ctx.beginPath();
        ctx.fillStyle = color;
        ctx.moveTo(x, y);
        ctx.arc(x, y, radius, 0, 2 * Math.PI);
        ctx.fill();
        ctx.closePath();

        points.push({ x, y });
      });

      // 绘制折线
      for (let i = 0; i < points.length - 1; i++) {
        const { x, y } = points[i];
        const nextPoint = points[i + 1];
        ctx.beginPath();
        ctx.strokeStyle = color;
        ctx.moveTo(x, y);
        ctx.lineTo(nextPoint.x, nextPoint.y);
        ctx.stroke();
        ctx.closePath();
      }
    });
  }

  /** 辅助函数 - 获取 Echart Bar 配置对象 */
  private getEchartBar(ctx: Konva.Context, shape: Konva.Shape) {
    const { width, height, xAxis } = shape.attrs;
    const { series, echartTitle = "", titleAlign = "center" } = shape.attrs;

    // 控制背景颜色
    ctx.beginPath();
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, width, height);
    ctx.fill();
    ctx.strokeStyle = "transparent";
    ctx.strokeRect(0, 0, width, height);
    ctx.stroke();
    ctx.closePath();

    /**
     * 定义标题居中方式 titleAlign = "left";
     * 是否平滑 smooth = false;
     * 是否显示值 label = true;
     * 是否显示图例 legend = true;
     */

    const seriesColor = <string[]>getColorMatch(this.draw, "seriesColor")!;

    // 取整个维度的最大值
    const smax = series.map((i: IKonva.EchartSeries) => Math.max(...i.data));
    // 约定 Y轴间隔线数量
    const gridLineNumber = 5;
    // 取整，避免出现小数
    let maxValue = Math.max(...smax);
    maxValue = Math.ceil(maxValue / gridLineNumber) * gridLineNumber; // 所以 整个轴线的高度 代表的值是 ===> maxValue

    // 动态决定四周的间距
    const paddingLeft = ctx.measureText(maxValue.toString()).width + 10; // 左侧的偏移量跟Y轴文本宽度有关
    const paddingRight = 10;
    const paddingBottom = 20;
    const paddingTop = echartTitle ? 58 : 40; // 顶部的偏移量与标题高度有关

    // 绘制标题
    if (echartTitle) {
      ctx.beginPath();
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesTitleColor");
      ctx.font = "18px Arial"; // 设置字体样式
      // 计算标题宽度
      const titleWidth = ctx.measureText(echartTitle).width;
      const left = 0;
      const center = width / 2 - titleWidth / 2;
      const right = width - titleWidth - paddingRight;
      const x =
        titleAlign === "left" ? left : titleAlign === "center" ? center : right; // 标题的位置 left center right
      ctx.fillText(echartTitle, x, 18);
      ctx.fill();
      ctx.closePath();
    }

    // 整个高度是 height-paddingTop-paddingBottom ==> maxValue  每一个间隔是 value/number  ygap 表示每一个小间隔的高度
    const ygap = (height - paddingTop - paddingBottom) / gridLineNumber;
    // 绘制 Y 轴间隔线 标线值
    for (let i = 0; i < gridLineNumber; i++) {
      // 绘制间隔线
      const y = paddingTop + i * ygap;
      ctx.beginPath();
      ctx.moveTo(paddingLeft, y);
      ctx.lineTo(width - paddingRight, y); // 右侧不受影响
      ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesGridColor");
      ctx.stroke();
      ctx.closePath();

      // 绘制文本
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
      ctx.font = "14px Arial";
      const text = (maxValue / gridLineNumber) * (gridLineNumber - i);
      ctx.fillText(text.toString(), 0, y + 5);
    }

    // 绘制 X 轴、X 轴间隔线、根据 xAxis.length 绘制类别名称
    const xgap = (width - paddingLeft - paddingRight) / xAxis.length;
    for (let i = 0; i < xAxis.length; i++) {
      const x = i * xgap + paddingLeft;
      const y = paddingBottom;

      // 绘制间隔
      ctx.beginPath();
      ctx.moveTo(x, height - y);
      ctx.lineTo(x, height - y * 0.4);
      ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
      ctx.stroke();
      ctx.closePath();

      // 放里面好折叠 无其他含义，下列代码与循环无关，确保执行一次即可
      if (i === 0) {
        // 如果是最后一个，多画一条末尾的间隔线
        ctx.beginPath();
        ctx.moveTo(width - paddingRight, height - y); // 最后一条线不受影响
        ctx.lineTo(width - paddingRight, height - y * 0.4); // 最后一条线不受影响
        ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
        ctx.stroke();
        ctx.closePath();
        // 绘制轴线 X 轴 -
        ctx.beginPath();
        ctx.moveTo(paddingLeft, height - paddingBottom);
        ctx.lineTo(width - paddingRight, height - paddingBottom); // 右侧不受影响
        ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
        ctx.stroke();
      }
      // 绘制文本
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
      ctx.font = "14px Arial"; // 设置字体样式
      // 计算文本宽度
      const textWidth = ctx.measureText(xAxis[i]).width;
      const textX = paddingLeft + i * xgap + xgap / 2 - textWidth / 2;
      const textY = height - y * 0.2;
      ctx.fillText(xAxis[i], textX, textY);
    }

    // 绘制 图例 - 根据 series 的 name 属性生成
    const allTextWidth = series.reduce(
      (pre: number, cur: IKonva.EchartSeries) => {
        const textWidth = ctx.measureText(cur.name).width + 40;
        return pre + textWidth;
      },
      0
    );

    series.forEach((item: IKonva.EchartSeries, index: number) => {
      const name = item.name;
      const y = echartTitle ? 36 : 18;
      // 获取当前文本宽度
      const getTextWidth = (ci: number) => {
        let allX = 0;
        for (let i = 0; i < ci; i++) {
          allX += ctx.measureText(series[i].name).width + 40;
        }
        return allX;
      };
      // x 坐标动态生成- 前面所有宽度之和
      const x = width / 2 - allTextWidth / 2 + getTextWidth(index);

      // 1. 绘制矩形
      ctx.beginPath();
      ctx.fillStyle = seriesColor[index];
      ctx.rect(x + 10, y - 5, 10, 10);
      ctx.fill();
      ctx.closePath();

      // 绘制文本
      ctx.fillStyle = seriesColor[index];
      ctx.fillText(name, x + 30, y + 5);
    });

    // 绘制柱状图 - 这里要考虑多个柱子公用一个 xgap 一个间隔要保留一个柱宽做间距
    const barWidth = xgap / (series.length + 1);

    series.forEach((item: IKonva.EchartSeries, index: number) => {
      const data = item.data;
      data.forEach((d: number, i: number) => {
        const x = paddingLeft + xgap * i + barWidth * index + barWidth / 2;
        const h = ((height - paddingBottom - paddingTop) / maxValue) * d;
        const y = height - paddingBottom - h;
        const w = barWidth;
        ctx.beginPath();
        ctx.fillStyle = seriesColor[index];
        ctx.rect(x, y, w, h);
        ctx.fill();
        ctx.closePath();
      });
    });
    ctx.fillStrokeShape(shape);
  }

  /** 辅助函数 - 获取 Echart Pie 配置对象 */
  private getEchartPie(ctx: Konva.Context, shape: Konva.Shape) {
    const { width, height, xAxis } = shape.attrs;
    const { series, echartTitle = "", titleAlign = "center" } = shape.attrs;

    // 控制背景颜色
    ctx.beginPath();
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, width, height);
    ctx.fill();
    ctx.strokeStyle = "transparent";
    ctx.strokeRect(0, 0, width, height);
    ctx.stroke();
    ctx.closePath();

    /**
     * 定义标题居中方式 titleAlign = "left";
     * 是否平滑 smooth = false;
     * 是否显示值 label = true;
     * 是否显示图例 legend = true;
     */

    const seriesColor = <string[]>getColorMatch(this.draw, "seriesColor")!;

    const paddingRight = 10;
    const paddingTop = echartTitle ? 58 : 40; // 顶部的偏移量与标题高度有关

    // 绘制 图例 - 根据 xAxis 属性生成
    const allTextWidth = xAxis.reduce((pre: string, cur: string) => {
      const textWidth = ctx.measureText(cur).width + 40;
      return pre + textWidth;
    }, 0);

    xAxis.forEach((name: string, index: number) => {
      const y = echartTitle ? 36 : 18;
      // 获取当前文本宽度
      const getTextWidth = (ci: number) => {
        let allX = 0;
        for (let i = 0; i < ci; i++) {
          allX += ctx.measureText(name).width + 40;
        }
        return allX;
      };
      // x 坐标动态生成- 前面所有宽度之和
      const x = width / 2 - allTextWidth / 2 + getTextWidth(index);

      ctx.beginPath();
      ctx.fillStyle = seriesColor[index];
      ctx.rect(x + 10, y - 5, 10, 10);
      ctx.fill();
      ctx.closePath();

      // 绘制文本
      ctx.fillStyle = seriesColor[index];
      ctx.fillText(name, x + 30, y + 5);
    });

    // 绘制标题
    if (echartTitle) {
      ctx.beginPath();
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesTitleColor");
      ctx.font = "18px Arial"; // 设置字体样式
      // 计算标题宽度
      const titleWidth = ctx.measureText(echartTitle).width;
      const left = 0;
      const center = width / 2 - titleWidth / 2;
      const right = width - titleWidth - paddingRight;
      const x =
        titleAlign === "left" ? left : titleAlign === "center" ? center : right; // 标题的位置 left center right
      ctx.fillText(echartTitle, x, 18);
      ctx.fill();
      ctx.closePath();
    }

    // 根据 series[0].data 绘制饼图 - 因为饼图不支持多系列对比
    const x = width / 2;
    const y = height / 2;
    const data = series[0].data;
    const radius = Math.min(width, height) / 2 - paddingTop;
    const sum = data.reduce((a: number, b: number) => a + b);
    // 角度与弧度的转换
    const getDegree = (value: number) => ((value / sum) * 360 * Math.PI) / 180;
    function getPointOnCircle(angle: number) {
      return {
        x: x + (radius - paddingTop / 2) * Math.cos(angle),
        y: y + (radius - paddingTop / 2) * Math.sin(angle),
      };
    }

    data.forEach((value: number, index: number) => {
      // 获取当前元素的起始角度 - 前面所有元素的角度之和
      const startAngle = index
        ? getDegree(
            data.slice(0, index).reduce((a: number, b: number) => a + b, 0)
          )
        : 0;

      const endAngle = startAngle + getDegree(value);

      // 开始绘制
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.fillStyle = seriesColor[index];
      ctx.arc(x, y, radius, startAngle, endAngle);
      ctx.fill();
      ctx.closePath();

      // 添加文字在外圈-中心
      if (xAxis[index]) {
        const point = getPointOnCircle((startAngle + endAngle) / 2);
        const text = xAxis[index];
        ctx.fillStyle = <string>getColorMatch(this.draw, "seriesAxisColor");
        ctx.fillText(
          value.toString(),
          point.x - ctx.measureText(text).width / 2,
          point.y + 20 * 0.2
        );
      }
    });

    ctx.fillStrokeShape(shape);
  }

  /** 辅助函数 - 获取Echart Radar 雷达图配置对象 */
  private getEchartRadar(ctx: Konva.Context, shape: Konva.Shape) {
    const { width, height, xAxis } = shape.attrs;
    const { series, echartTitle = "", titleAlign = "center" } = shape.attrs;

    // 控制背景颜色
    ctx.beginPath();
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, width, height);
    ctx.fill();
    ctx.strokeStyle = "transparent";
    ctx.strokeRect(0, 0, width, height);
    ctx.stroke();
    ctx.closePath();

    /**
     * 定义标题居中方式 titleAlign = "left";
     * 是否平滑 smooth = false;
     * 是否显示值 label = true;
     * 是否显示图例 legend = true;
     */

    const seriesColor = <string[]>getColorMatch(this.draw, "seriesColor")!;

    // 取整个维度的最大值
    const smax = series.map((i: IKonva.EchartSeries) => Math.max(...i.data));
    // 约定 Y轴间隔线数量
    const gridLineNumber = 5;
    // 取整，避免出现小数
    let maxValue = Math.max(...smax);
    maxValue = Math.ceil(maxValue / gridLineNumber) * gridLineNumber; // 所以 整个轴线的高度 代表的值是 ===> maxValue

    // 绘制轴线 - 将圆平均分成 xAxis 份
    const paddingRight = 10;
    const paddingTop = echartTitle ? 58 : 40; // 顶部的偏移量与标题高度有关
    const x = width / 2;
    const y = height / 2;
    const radius = (Math.min(width, height) - paddingTop) / 2; // 雷达半径 = 最小值 - paddingTop
    // 每一份所占的角度
    const angle = (Math.PI * 2) / xAxis.length;

    // 绘制 图例 - 根据 series 的 name 属性生成
    const allTextWidth = series.reduce(
      (pre: number, cur: IKonva.EchartSeries) => {
        const textWidth = ctx.measureText(cur.name).width + 40;
        return pre + textWidth;
      },
      0
    );

    series.forEach((item: IKonva.EchartSeries, index: number) => {
      const name = item.name;
      const y = echartTitle ? 36 : 18;
      // 获取当前文本宽度
      const getTextWidth = (ci: number) => {
        let allX = 0;
        for (let i = 0; i < ci; i++) {
          allX += ctx.measureText(series[i].name).width + 40;
        }
        return allX;
      };
      // x 坐标动态生成- 前面所有宽度之和
      const x = width / 2 - allTextWidth / 2 + getTextWidth(index);

      // 1. 绘制矩形
      ctx.beginPath();
      ctx.fillStyle = seriesColor[index];
      ctx.rect(x + 10, y - 5, 10, 10);
      ctx.fill();
      ctx.closePath();

      // 绘制文本
      ctx.fillStyle = seriesColor[index];
      ctx.fillText(name, x + 30, y + 5);
    });

    // 绘制标题
    if (echartTitle) {
      ctx.beginPath();
      ctx.fillStyle = <string>getColorMatch(this.draw, "seriesTitleColor");
      ctx.font = "18px Arial"; // 设置字体样式
      // 计算标题宽度
      const titleWidth = ctx.measureText(echartTitle).width;
      const left = 0;
      const center = width / 2 - titleWidth / 2;
      const right = width - titleWidth - paddingRight;
      const x =
        titleAlign === "left" ? left : titleAlign === "center" ? center : right; // 标题的位置 left center right
      ctx.fillText(echartTitle, x, 18);
      ctx.fill();
      ctx.closePath();
    }

    // 先绘制雷达
    for (let i = 0; i < gridLineNumber - 1; i++) {
      const r = radius - (radius / gridLineNumber) * i;
      const point = getPointOnCircle(-Math.PI / 2, r);
      ctx.beginPath();
      ctx.moveTo(point.x, point.y);
      for (let j = 1; j < xAxis.length; j++) {
        const nextPoint = getPointOnCircle(j * angle - Math.PI / 2, r);
        ctx.lineTo(nextPoint.x, nextPoint.y);
      }
      ctx.closePath();
      ctx.fillStyle = i % 2 ? "#fff" : "#F6F8FC";
      ctx.fill();
      ctx.strokeStyle = "rgba(0,0,0,0.1)";
      ctx.stroke();
    }

    // 绘制轴线
    function getPointOnCircle(angle: number, r?: number) {
      return {
        x: x + ((r || radius) - paddingTop / 2) * Math.cos(angle),
        y: y + ((r || radius) - paddingTop / 2) * Math.sin(angle),
      };
    }
    xAxis.forEach((name: string, index: number) => {
      const point = getPointOnCircle(angle * index - Math.PI / 2);
      // 第一个点就是从 顶部开始
      ctx.beginPath();
      ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesGridColor");
      ctx.moveTo(x, y);
      ctx.lineTo(point.x, point.y);
      ctx.stroke();
      ctx.closePath();

      // 顶点处绘制文本
      ctx.fillStyle = "#000";
      ctx.fillText(
        name,
        point.x - ctx.measureText(name).width / 2,
        point.y + 20 * 0.2
      );
    });

    // 绘制内容雷达
    series.forEach((series: IKonva.EchartSeries, index: number) => {
      const data = series.data;
      const firstPoint = getPointOnCircle(
        -Math.PI / 2,
        (data[0] * radius) / maxValue
      );
      ctx.beginPath();
      ctx.moveTo(firstPoint.x, firstPoint.y);
      const points = [firstPoint];

      for (let j = 1; j < data.length; j++) {
        const nextPoint = getPointOnCircle(
          j * angle - Math.PI / 2,
          (data[j] * radius) / maxValue
        );
        points.push(nextPoint);
        ctx.lineTo(nextPoint.x, nextPoint.y);
      }
      ctx.closePath();

      const { r, g, b } = Konva.Util._hexToRgb(
        <string>getColorMatch(this.draw, "seriesColor")![index]
      );
      ctx.fillStyle = `rgba(${r},${g},${b},0.5)`;
      ctx.fill();
      ctx.strokeStyle = <string>getColorMatch(this.draw, "seriesColor")![index];
      ctx.stroke();

      // 绘制点
      points.forEach((point: { x: number; y: number }) => {
        ctx.beginPath();
        ctx.arc(point.x, point.y, 2, 0, Math.PI * 2);
        ctx.fillStyle = <string>getColorMatch(this.draw, "seriesColor")![index];
        ctx.fill();
        ctx.closePath();
      });
    });
  }

  /** 辅助函数 - 获取箭头配置对象 */
  private getArrowOption(ctx: Konva.Context, shape: Konva.Shape) {
    const { width = 1, height = 1, insertShape = "arrow1" } = shape.attrs;
    /**
     * ➡⬆⬇⬅ 外加特殊的箭头，通过 mousedown 事件传递到 shape.attrs 上
     */
    ctx.fillStyle = <string>getColorMatch(this.draw, "nodeFillColor");
    ctx.beginPath();

    switch (insertShape) {
      case "arrow1":
        // 向上箭头
        ctx.moveTo(width * 0.5, 0);
        ctx.lineTo(width, height * 0.25);
        ctx.lineTo(width * 0.75, height * 0.25);
        ctx.lineTo(width * 0.75, height);
        ctx.lineTo(width * 0.25, height);
        ctx.lineTo(width * 0.25, height * 0.25);
        ctx.lineTo(0, height * 0.25);

        break;
      case "arrow2":
        // 向下箭头
        ctx.moveTo(width * 0.25, 0);
        ctx.lineTo(width * 0.75, 0);
        ctx.lineTo(width * 0.75, height * 0.75);
        ctx.lineTo(width, height * 0.75);
        ctx.lineTo(width * 0.5, height);
        ctx.lineTo(0, height * 0.75);
        ctx.lineTo(width * 0.25, height * 0.75);
        break;
      case "arrow3":
        // 向左箭头
        ctx.moveTo(0, height * 0.5);
        ctx.lineTo(width * 0.25, height);
        ctx.lineTo(width * 0.25, height * 0.75);
        ctx.lineTo(width, height * 0.75);
        ctx.lineTo(width, height * 0.25);
        ctx.lineTo(width * 0.25, height * 0.25);
        ctx.lineTo(width * 0.25, 0);
        ctx.lineTo(0, height * 0.5);
        break;
      case "arrow4":
        // 向右箭头
        ctx.moveTo(0, height * 0.25);
        ctx.lineTo(0, height * 0.75);
        ctx.lineTo(width * 0.75, height * 0.75);
        ctx.lineTo(width * 0.75, height);
        ctx.lineTo(width, height * 0.5);
        ctx.lineTo(width * 0.75, 0);
        ctx.lineTo(width * 0.75, height * 0.25);

        break;
      case "arrow5":
        // 向右开口箭头
        ctx.moveTo(0, height * 0.25);
        ctx.lineTo(width * 0.25, height * 0.5);
        ctx.lineTo(0, height * 0.75);
        ctx.lineTo(width * 0.75, height * 0.75);
        ctx.lineTo(width * 0.75, height);
        ctx.lineTo(width, height * 0.5);
        ctx.lineTo(width * 0.75, 0);
        ctx.lineTo(width * 0.75, height * 0.25);
        break;
      case "arrow6":
        // 向右矩形箭头
        ctx.moveTo(0, 0);
        ctx.lineTo(width * 0.75, 0);
        ctx.lineTo(width, height * 0.5);
        ctx.lineTo(width * 0.75, height);
        ctx.lineTo(0, height);
        ctx.lineTo(width * 0.25, height * 0.5);

        break;
      case "arrow7":
        // 向右三角箭头
        ctx.moveTo(0, 0);
        ctx.lineTo(width, height * 0.5);
        ctx.lineTo(0, height);
        ctx.lineTo(width * 0.25, height * 0.5);
        break;
      case "arrow8":
        ctx.moveTo(0, 0);
        ctx.lineTo(width * 0.75, 0);
        ctx.lineTo(width, height * 0.5);
        ctx.lineTo(width * 0.75, height);
        ctx.lineTo(0, height);
        break;

      default:
        break;
    }

    ctx.closePath();
    ctx.fill();
    ctx.fillStrokeShape(shape);
  }

  /** 辅助函数 - 绘制 logo */
  private getLogoOptions(ctx: Konva.Context, shape: Konva.Shape) {
    const { width, height } = shape.size();
    // 定义圆弧大小
    const radius = 10;
    // 绘制矩形
    ctx.beginPath();
    ctx.fillStyle = <string>getColorMatch(this.draw, "nodeFillColor");
    ctx.moveTo(radius, 0);
    ctx.lineTo(width - radius, 0);
    // 绘制右上圆角
    ctx.arc(width - radius, radius, radius, Math.PI * 1.5, Math.PI * 2);
    ctx.lineTo(width, height - radius);
    // 绘制右下圆角
    ctx.arc(width - radius, height - radius, radius, 0, Math.PI * 0.5);
    ctx.lineTo(radius, height);
    // 绘制左下圆角
    ctx.arc(radius, height - radius, radius, Math.PI * 0.5, Math.PI);
    ctx.lineTo(0, radius);
    // 绘制左上圆角
    ctx.arc(radius, radius, radius, Math.PI, Math.PI * 1.5);
    ctx.closePath();
    ctx.fill();

    // 绘制内部文字 - P
    ctx.fillStyle = <string>getColorMatch(this.draw, "nodeTextColor");
    ctx.font = "bold 60px Arial";
    ctx.textBaseline = "bottom";
    ctx.fillText(
      "P",
      width * 0.5 - ctx.measureText("p").width / 2,
      height * 0.5 + ctx.measureText("p").fontBoundingBoxAscent / 2
    );

    ctx.fillStrokeShape(shape);
  }

  /** 辅助函数 - 绘制 Triangle 三角形 */
  private getTriangleOption(ctx: Konva.Context, shape: Konva.Shape) {
    // 根据宽高绘制圆角三角形即可
    const { width, height } = shape.size();
    ctx.beginPath();
    ctx.moveTo(width * 0.5, 0);
    ctx.lineTo(width, height);
    ctx.lineTo(0, height);
    ctx.closePath();
    ctx.fillStyle = <string>getColorMatch(this.draw, "nodeFillColor");
    ctx.fill();
    ctx.fillStrokeShape(shape);
  }

  /** 辅助函数： 为了使得 自定义的图形能够自适应事件响应区域 */
  private hitFunc(ctx: Konva.Context, shape: Konva.Shape) {
    const { width, height } = shape.size();
    ctx.beginPath();
    ctx.rect(0, 0, width, height);
    ctx.closePath();
    ctx.fillStrokeShape(shape);
  }

  /**
   * 每一个 Konva Graph 都需要一个 Group 包裹，
   * group 的宽高不一定直接是 width,可能是 radius 或者 radiusX,radiusY，为了适应不同的元素类型
   */
  private getGroup(payload: Konva.NodeConfig) {
    const { x = 0, y = 0, radius, radiusX, radiusY, outerRadius } = payload;
    // 处理宽高默认值 - 适应圆、椭圆等场景
    const width =
      payload.width || radius * 2 || radiusX * 2 || outerRadius * 2 || 0;
    const height =
      payload.height || radius * 2 || radiusY * 2 || outerRadius * 2 || 0;

    const baseOption = { x, y, width, height, draggable: true };
    const group = new Konva.Group({
      id: payload.id || getUniqueId(),
      ...payload,
      ...baseOption,
      name: KONVA_GRAPH_NAME, // 经过封装，添加名称属性，方便 layer.find('.name')
    });
    return group;
  }

  /**
   * 这个是每一个可以双击进行文本编辑的元素
   * 内部的双击文本输入框通过 contexteditable 实现
   */
  private getGroupText(payload: Konva.NodeConfig) {
    // 为了避免 payload 中的参数影响文本显示效果，应该直接解析 text 的配置项
    const { height, width, text = "", text_fill, text_fontSize } = payload;
    const konvaText = new Konva.Text({
      ellipsis: true,
      verticalAlign: "middle", // 水平垂直居中
      align: "center", // 水平垂直居中
      lineHeight: 1.5, // 行高
      fontSize: text_fontSize || 16, // 字体大小
      fill: text_fill || this.nodeTextColor,
      text,
      height,
      width,
      x: 0,
      y: 0,
      offsetX: 0,
      offsetY: 0,
    });
    return konvaText;
  }

  /**
   * 为了实现数据导入导出，需要识别导出的 json 数据类型
   */
  public createKonvaShape(oldGroup: Konva.Group): Konva.Group {
    const group = this.getGroup({ ...oldGroup.attrs, id: getUniqueId() });

    // 先不处理 bgGroup

    if (oldGroup.attrs.name === KONVA_GRAPH_NAME) {
      // 根据 oldGroup 的 children 判断应该是何种类型
      oldGroup.children.forEach((shape) => {
        const { className } = shape;
        const payload = shape.attrs;
        const { shapeType } = oldGroup.attrs;
        // 这里识别的是真实的底层图形  而不是group哈
        // 派生事件处理机制
        if (className === "Rect") return group.add(new Konva.Rect(payload));
        if (className === "Text") return group.add(new Konva.Text(payload));
        if (className === "Star") return group.add(new Konva.Star(payload));
        if (className === "Ring") return group.add(new Konva.Ring(payload));
        if (className === "Circle") return group.add(new Konva.Circle(payload));
        if (className === "Ellipse") {
          return group.add(new Konva.Ellipse(payload));
        }
        if (className === "Shape") {
          // Shape 指的是 canvas 绘制的内容
          type SceneFunc = (ctx: Konva.Context, shape: Konva.Shape) => void;
          // 绘制函数映射
          const SFM: IKeyMap<string, SceneFunc> = {
            Table: this.getTableOption.bind(this),
            Echart: this.getEchartOption.bind(this),
            Logo: this.getLogoOptions.bind(this),
            Triangle: this.getTriangleOption.bind(this),
          };
          const sceneFunc = SFM[shapeType] || this.getArrowOption.bind(this);
          const newShape = new Konva.Shape({
            ...shape.attrs,
            sceneFunc,
            hitFunc: this.hitFunc,
          });

          return group.add(newShape);
        }
        if (className === "Image") {
          // 这里拿 source 进行重新生成 image 即可，区分图片 视频
          if (shapeType === "Image") {
            const image = new Image();
            image.src = oldGroup.attrs.source;
            image.onload = () => {
              const imageOption = { ...payload, image, x: 0, y: 0 };
              const konvaImage = new Konva.Image(imageOption);
              group.add(konvaImage);
              this.draw.render();
            };
          }
        }
      });
    }

    this.overwriteGraph(group);
    return group;
  }

  /**
   * 重写 Konva.Line 相关事件，不使用 Group 实现
   * @param Konva.Line
   */
  public overwriteLine(line: Konva.Line) {
    line.on("click", (e) => lineClick(e, this.draw));
    line.on("dragstart", () => lineDragStart(this.draw));
    // line.on("dragmove", (e) => lineDragMove(e, this.draw));
    line.on("dragend", (e) => lineDragEnd(e, this.draw));
    line.on("mouseenter", () => lineMouseEnter(this.draw));
    line.on("mouseleave", () => lineMouseLeave(this.draw));
  }

  /**
   * 重写 Konva.Node 的事件，以实现更多功能
   * @param graph
   */
  public overwriteGraph(graph: Konva.Node) {
    graph.on("click", (e) => groupClick(e, this.draw));
    graph.on("dragstart", () => groupDragStart(this.draw));
    graph.on("dragmove", (e) => groupDragMove(e, this.draw));
    graph.on("dragend", (e) => groupDragend(e, this.draw));
    graph.on("mouseenter", () => groupMouseEnter(this.draw));
    graph.on("mouseleave", () => groupMouseLeave(this.draw));
    graph.on("dblclick", (e) => groupDbclick(e, this.draw));
  }

  /**
   * 取消相关事件
   */
  public cancelEvent(graph: Konva.Node) {
    graph.off("click");
    graph.off("dragstart");
    graph.off("dragmove");
    graph.off("dragend");
    graph.off("mouseenter");
    graph.off("mouseleave");
    graph.off("dblclick");
  }
}
